<?php

namespace W3;

/**
 * 系统工具类
 *
 * @author edikud
 * @date 2022/10/22
 * @copyright Copyright (c) 2022 W3 (http://www.mcooo.com)
 * @license GNU General Public License 2.0
 */

class Util
{
	
    /**
     * 事件映射数组
     *
     * @var array
     */
    public static $events = [];
	
    /**
     * 缓存的插件配置
     * @var []
     */
    public static $pluginConfig = [];

    /**
     * 多维数组递归合并
     *
     *	 
     * $ar1 = array("color" => array("red", "green"), "aa");
     * $ar2 = array("color" => array( "green", "blue"), "bb");	 
     * 	 
     * $result = array_merge_recursive($ar1, $ar2);	 
     * 	 
     * 输出:	 
     * [
     *     'color' => [
     *         (int) 0 => 'red',
     *         (int) 1 => 'green',
     *         (int) 2 => 'green', (!)
     *         (int) 3 => 'blue'
     *     ],
     *     (int) 0 => 'aa',
     *     (int) 1 => 'bb'
     * ]	 
     * 	 
     * 	 
     * $result = Util::arrayMerge($ar1, $ar2); 需要的预期输出
     * 	 
     * 	 输出:
     * [
     *     'color' => [
     *         (int) 0 => 'red',
     *         (int) 1 => 'green',
     *         (int) 3 => 'blue'
     *     ],
     *     (int) 0 => 'aa',
     *     (int) 1 => 'bb'
     * ]	 
     * 	 
     * 
     * @param array $array1
     * @param array $array2
     * @return array
     */
    public static function arrayMerge(array $array1, array $array2): array
    {
        $merged = $array1;

        foreach ($array2 as $key => & $value) 
		{
            if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
                $merged[$key] = static::arrayMerge($merged[$key], $value);
            } else if (is_numeric($key)) {
				
				# 数字键名
                if (!in_array($value, $merged)) {
                    $merged[] = $value;
                }
            } else {
                $merged[$key] = $value;
            }
        }

        return $merged;
    }

    /**
     * 多维数组递归合并默认数据
     * 如果一个索引键存在于第一个数组同时也存在于第二个数组，它的值将不被第二个数组中的值替换 (值是数组时替换)
     * 如果数组包含数字键名，后面的值将 不会 覆盖原来的值，而是附加到后面
	 *
	 * $ar1 = array("a" =>'dd', "color" => array("red", "green", "color" => array("red", "green")), "aa");
     * $ar2 = array("a" =>['ccc'], "color" => array( "green", "blue", "color" => array("blue", "green")), "bb"); 
     * 	 
     * $result = Util::arrayDiffMerge($ar1, $ar2); 需要的预期输出
     * 	 
     * 输出:
     * [
     *   [a] => [
     *      [0] => ccc
     *    ]
     * 
     *     [color] => [
     *             [0] => red
     *             [1] => green
     *             [color] => [
     *                     [0] => red
     *                     [1] => green
     *                     [2] => blue
     *                 ]
     * 
     *             [2] => blue
     *         ]
     * 
     *     [0] => aa
     *     [1] => bb
     * ]	 
     * 	 
     * 
     * @param array $array1
     * @param array $array2
     * @return array
     */
    public static function arrayDiffMerge(array $array1, array $array2): array
    {
        $merged = $array1;

        foreach ($array2 as $key => & $value) 
		{
            if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
                $merged[$key] = static::arrayDiffMerge($merged[$key], $value);
            } else if (is_numeric($key)) {
				
				# 数字键名
                if (!in_array($value, $merged)) {
                    $merged[] = $value;
                }
            } else {
				# 数组时替换
				(!isset($merged[$key]) || is_array($value)) &&
					$merged[$key] = $value;
            }
        }

        return $merged;
    }


    /**
     * 按指定键提取数组元素
     * 
     * @param array $array
     * @param array $keys
     * @return array
     */
    public static function arrayExtract(array $array, array $keys): array
    {
        $result = [];
        foreach ($keys as $key)
	    {
            if (isset($array[$key])) {
                $result[$key] = $array[$key];
            }
        }
    
        return $result;
    }

    /**
	 * 对多维数组排序
	 *
	 * $data = [];
	 * $data[] = ['volume' => 67, 'edition' => 2];
	 * $data[] = ['volume' => 86, 'edition' => 1];
	 * $data[] = ['volume' => 85, 'edition' => 6];
	 * $data[] = ['volume' => 98, 'edition' => 2];
	 * $data[] = ['volume' => 86, 'edition' => 6];
	 * $data[] = ['volume' => 67, 'edition' => 7];
	 * array_multisort($data, 'edition', TRUE);
     */
    public static function arrayMultiSort(array $array, string $col, bool $asc = TRUE): array
    {
		// 索引计数器 [解决在值相同时改变原始位置的问题]
        $i = 0;
        $index = $colarr = [];
        foreach ($array as $k => $arr) 
		{
            $colarr[$k] = $arr[$col];
			$index[] = $i++;
        }
        $asc = $asc ? SORT_ASC : SORT_DESC;
        array_multisort($colarr, $asc, $index, $asc, $array);
        return $array;
    }
	
    /**
     * 根据parse_url的结果重新组合url
     *
     * @access public
     * @param array $params 解析后的参数
     * @return string
     */
    public static function buildUrl(array $params): ?string
    {
        return (isset($params['scheme']) ? $params['scheme'] . '://' : NULL)
        . (isset($params['user']) ? $params['user'] . (isset($params['pass']) ? ':' . $params['pass'] : NULL) . '@' : NULL)
        . (isset($params['host']) ? $params['host'] : NULL)
        . (isset($params['port']) ? ':' . $params['port'] : NULL)
        . (isset($params['path']) ? $params['path'] : NULL)
        . (isset($params['query']) ? '?' . $params['query'] : NULL)
        . (isset($params['fragment']) ? '#' . $params['fragment'] : NULL);
    }
	
    /**
     * 对比两个url (!)
     *
     * @param string $url url
     * @return string
     */
    public static function compareUrl($url, $currentUrl = null)
    {
		//static $static = [];
		
		$currentUrl = $currentUrl ?? rtrim(Request::instance()->url(true), '/');
        $currentUrlParts = parse_url($currentUrl);
        $currentUrlParams = [];
        if (!empty($currentUrlParts['query'])) {
            parse_str($currentUrlParts['query'], $currentUrlParams);
        }

        # compare url
        $urlParts = parse_url(rtrim($url, '/'));
        $urlParams = [];
        if (!empty($urlParts['query'])) {
            parse_str($urlParts['query'], $urlParams);
        }

        $validate = true;

        if ($urlParts['path'] != $currentUrlParts['path']) {
            $validate = false;
        } else {
            foreach ($urlParams as $paramName => $paramValue) 
		    {
                if (!isset($currentUrlParams[$paramName])) {
                    $validate = false;
                    break;
                }
            }
        }
        return $validate;
    }
	
    /**
     * 生成缩略名
     *
     * @access public
     * @param string $str 需要生成缩略名的字符串
     * @return string
     */
    public static function alias(?string $alias = NULL)
    {
		if(empty($alias)) 
			return base_convert(sprintf("%u", crc32(uniqid(mt_rand(), true))), 10, 36);
		
		return ctype_alnum(str_replace(['-', '_'], '', $alias)) ? 
		$alias : base_convert(sprintf("%u", crc32($alias)), 10, 36);
	}
	
    /**
     * 将一个平面的二维数组按照指定的字段转换为树状结构
     * 
     * @param array $arrlist 数据源
     * @param string $column_id 分类ID字段名
     * @param string $column_pid 分类父ID字段名
     *
     * return array 树形结构的数组
     */
    public static function arrayTree(array $arrlist, string $column_id = 'cid', string $column_pid = 'pid'): array
    {
        $result = [];
        if (empty($arrlist)) {
            return $result;
        }
		
		$arrlist[] = [$column_id => $column_id, $column_pid => 0] + $arrlist[0];
        $count = count($arrlist);
        $pid = 0;
        # 辅助数组
        $arr_assert = [];
        $arr_new = [];
        foreach ($arrlist as $value) 
		{
            # 层级
            $value['level'] = 0;
            # 自己及其子代的数量
            $value['num'] = 1;
            $arr_new[$value[$column_id]] = $value;
			$parent = $value[$column_pid];
            if (!isset($arr_assert[$parent])) {
                $arr_assert[$parent]['start'] = 0;
                $arr_assert[$parent]['end'] = 0;
            }
            $arr_assert[$parent][] = $value[$column_id];
            ++$arr_assert[$parent]['end'];
        }
        $offset = 0;
        $level = 0;
        while ($offset < $count) {
            if (isset($arr_assert[$pid]) && $arr_assert[$pid]['start'] < $arr_assert[$pid]['end']) {
                $index = $arr_assert[$pid]['start'];
                ++$arr_assert[$pid]['start'];
                $pid = $arr_assert[$pid][$index];
                $arr_new[$pid]['offset'] = $offset;
                $arr_new[$pid]['level'] = $level;
                $result[$pid] =& $arr_new[$pid];
                ++$level;
                ++$offset;
            } else {
                --$level;
                $num = $arr_new[$pid]['num'];
                $pid = $arr_new[$pid][$column_pid];
                if ($pid) {
                    $arr_new[$pid]['num'] += $num;
                }
            }
        }
        $result[$column_id] = null;
        unset($result[$column_id]);
        return $result;
    }


    /**
     * 检测是否在app engine上运行，屏蔽某些功能 (!)
     * 
     * @static
     * @access public
     * @return boolean
     */
    public static function isAppEngine()
    {
        return !empty($_SERVER['HTTP_APPNAME'])                     // SAE
            || !!getenv('HTTP_BAE_ENV_APPID')                       // BAE
            || !!getenv('HTTP_BAE_LOGID')                           // BAE 3.0
            || (isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'],'Google App Engine') !== false) // GAE
            ;
    }
    /**
     * 生成随机字符串
     *
     * @access public
     * @param integer $length 字符串长度
     * @param boolean $specialChars 是否有特殊字符
     * @return string
     */
    public static function randstring(int $length = 16, string $chars = '0123456789abcdefghijklmnopqrstuvwxyz'): string
    {
        $len = strlen($chars);
        $return = '';
        for ($i = 0; $i < $length; $i++) {
            $r = rand(1, $len);
            $return .= $chars[$r - 1];
        }
        return $return;
    }	

    /**
     * 对字符串进行hash加密
     *
     * @access public
     * @param string $string 需要hash的字符串
     * @param string $salt 扰码
     * @return string
     */
    public static function hash(?string $string, ?string $salt = NULL): string
    {
        if (!isset($string)) {
            return '';
        }
		
        /** 生成随机字符串 */
        $salt = empty($salt) ? static::randstring(9) : $salt;
        $length = strlen($string);
        $hash = '';
        $last = ord($string[$length - 1]);
        $pos = 0;

        /** 判断扰码长度 */
        if (strlen($salt) != 9) {
            /** 如果不是9直接返回 */
            return '';
        }

        while ($pos < $length) {
            $asc = ord($string[$pos]);
            $last = ($last * ord($salt[($last % $asc) % 9]) + $asc) % 95 + 32;
            $hash .= chr($last);
            $pos ++;
        }

        return '$T$' . $salt . md5($hash);
    }

    /**
     * 判断hash值是否相等
     *
     * @access public
     * @param string $from 源字符串
     * @param string $to 目标字符串
     * @return boolean
     */
    public static function hashValidate(?string $from, ?string $to): bool
    {
        if (!isset($from) || !isset($to)) {
            return false;
        }
			
        if ('$T$' == substr($to, 0, 3)) {
            $salt = substr($to, 3, 9);
            return static::hash($from, $salt) === $to;
        } else {
            return static::passwordHash($from) === $to;
        }
    }

    # 创建和校验哈希密码
    public static function passwordHash($password)
    {
		$md5password = md5($password);
		$salt = substr($md5password, 3, 19);
        return substr(sha1($md5password . $salt), 3, 32);
    }
	
    /**
     * 根据count数目来输出字符
     * <code>
     * echo split_count(20, 10, 20, 30, 40, 50);
     * </code>
     *
     * @access public
     * @param int $count
     * @return string
     */
    public static function splitCount($count)
    {
        $sizes = func_get_args();
        array_shift($sizes);

        foreach ($sizes as $size) 
		{
            if ($count < $size) {
                return $size;
            }
        }

        return 0;
    }
	
    public static function urlEncode($s)
    {
        $s = str_replace('-', '_2d', $s);
        $s = str_replace('.', '_2e', $s);
        $s = str_replace('+', '_2b', $s);
        $s = str_replace('=', '_3d', $s);
        $s = urlencode($s);
        $s = str_replace('%', '_', $s);
        return $s;
    }
	
    public static function urlDecode($s)
    {
        $s = str_replace('_', '%', $s);
        $s = urldecode($s);
        return $s;
    }

    # 递归加反斜线
    public static function addslashes($var)
    {
        if (is_array($var)) {
            foreach ($var as $k => $v) {
                static::addslashes($v);
            }
        } else {
            $var = addslashes($var);
        }
        return $var;
    }
	
    # 递归清理反斜线
    public static function stripslashes($var)
    {
        if (is_array($var)) {
            foreach ($var as $k => $v) {
                static::stripslashes($v);
            }
        } else {
            $var = stripslashes($var);
        }
        return $var;
    }

    /**
      * 宽字符串截字函数
      *
      * @param string $str 需要截取的字符串
      * @param integer $start 开始截取的位置
      * @param integer $length 需要截取的长度
      * @param string $trim 截取后的截断标示符
      * @return string
      */
    public static function substr($str, $start, $length, $trim = "...")
    {
        if (!strlen($str)) {
            return '';
        }
        $iLength = static::length($str) - $start;
        $tLength = $length < $iLength ? $length - static::length($trim) : $length;
        $str = mb_substr($str, $start, $tLength, 'UTF-8');
        return $length < $iLength ? $str . $trim : $str;
    }

    /**
     * 获取大写字符串
     * 
     * @param string $str 
     * @access public
     * @return string
     */
    public static function upper($str)
    {
        return mb_strtoupper($str, 'UTF-8');
    }
	
    /**
     * 获取宽字符串长度函数
     *
     * @param string $str 需要获取长度的字符串
     * @param  string|null  $encoding
     * @return int
     */
    public static function length($str)
    {
        return mb_strlen($str, 'UTF-8');
    }	
	
    # 递归转换为HTML实体代码
    public static function safeHtml($var)
    {
        if (is_array($var)) {
            foreach ($var as $k => $v) {
                static::safeHtml($v);
            }
        } else {
            $var = str_replace(['&', '"', '<', '>'], ['&amp;', '&quot;', '&lt;', '&gt;'], $var);
        }
        return $var;
    }
	
    /**
     * 将url中的非法字符串
     *
     * @param string $url 需要过滤的url
     * @return string
     */
    public static function safeUrl($url)
    {
        //~ 针对location的xss过滤, 因为其特殊性无法使用remove_xss函数
        //~ fix issue 66
        $params = parse_url(str_replace(["\r", "\n", "\t", ' '], '', $url));

        /** 禁止非法的协议跳转 */
        if (isset($params['scheme'])) {
            if (!in_array($params['scheme'], ['http', 'https'])) {
                return '/';
            }
        }

        /** 过滤解析串 */
        $params = array_map('\W3\Util::removeUrlXss', $params);
        return static::buildUrl($params);
    }

    /**
     * 将url中的非法xss去掉时的数组回调过滤函数
     *
     * @access private
     * @param string $string 需要过滤的字符串
     * @return string
     */
    public static function removeUrlXss($string)
    {
        $string = str_replace(['%0d', '%0a'], '', strip_tags($string));
        return preg_replace([
            "/\(\s*(\"|')/i",           //函数开头
            "/(\"|')\s*\)/i",           //函数结尾
        ], '', $string);
    }	
	
    /**
     * Remove a value from the start of a string
     * in this case the passed URI string
     *
     * @param string $value value to remove
     * @param string $uri   request URI to remove the value from
     *
     * @return string processed request URI string
     */
    public static function replaceStart($find, $string)
    {
        // make sure our search value is a non-empty string
        if (!empty($find)) {

            // if the search value is at the start sub it out
            if (strpos($string, $find) === 0) {
                $string = substr($string, strlen($find));
            }
        }

        return $string;
    }

	
    /**
     * 处理XSS跨站攻击的过滤函数
     *
     * @author kallahar@kallahar.com
     * @link http://kallahar.com/smallprojects/php_xss_filter_function.php
     * @access public
     * @param string $val 需要处理的字符串
     * @return string
     */
    public static function removeXss($val)
    {
       // remove all non-printable characters. CR(0a) and LF(0b) and TAB(9) are allowed
       // this prevents some character re-spacing such as <java\0script>
       // note that you have to handle splits with \n, \r, and \t later since they *are* allowed in some inputs
       $val = preg_replace('/([\x00-\x08]|[\x0b-\x0c]|[\x0e-\x19])/', '', $val);

       // straight replacements, the user should never need these since they're normal characters
       // this prevents like <IMG SRC=&#X40&#X61&#X76&#X61&#X73&#X63&#X72&#X69&#X70&#X74&#X3A&#X61&#X6C&#X65&#X72&#X74&#X28&#X27&#X58&#X53&#X53&#X27&#X29>
       $search = 'abcdefghijklmnopqrstuvwxyz';
       $search .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
       $search .= '1234567890!@#$%^&*()';
       $search .= '~`";:?+/={}[]-_|\'\\';

       for ($i = 0; $i < strlen($search); $i++) {
          // ;? matches the ;, which is optional
          // 0{0,7} matches any padded zeros, which are optional and go up to 8 chars

          // &#x0040 @ search for the hex values
          $val = preg_replace('/(&#[xX]0{0,8}'.dechex(ord($search[$i])).';?)/i', $search[$i], $val); // with a ;
          // &#00064 @ 0{0,7} matches '0' zero to seven times
          $val = preg_replace('/(&#0{0,8}'.ord($search[$i]).';?)/', $search[$i], $val); // with a ;
       }

       // now the only remaining whitespace attacks are \t, \n, and \r
       $ra1 = Array('javascript', 'vbscript', 'expression', 'applet', 'meta', 'xml', 'blink', 'link', 'style', 'script', 'embed', 'object', 'iframe', 'frame', 'frameset', 'ilayer', 'layer', 'bgsound', 'title', 'base');
       $ra2 = Array('onabort', 'onactivate', 'onafterprint', 'onafterupdate', 'onbeforeactivate', 'onbeforecopy', 'onbeforecut', 'onbeforedeactivate', 'onbeforeeditfocus', 'onbeforepaste', 'onbeforeprint', 'onbeforeunload', 'onbeforeupdate', 'onblur', 'onbounce', 'oncellchange', 'onchange', 'onclick', 'oncontextmenu', 'oncontrolselect', 'oncopy', 'oncut', 'ondataavailable', 'ondatasetchanged', 'ondatasetcomplete', 'ondblclick', 'ondeactivate', 'ondrag', 'ondragend', 'ondragenter', 'ondragleave', 'ondragover', 'ondragstart', 'ondrop', 'onerror', 'onerrorupdate', 'onfilterchange', 'onfinish', 'onfocus', 'onfocusin', 'onfocusout', 'onhelp', 'onkeydown', 'onkeypress', 'onkeyup', 'onlayoutcomplete', 'onload', 'onlosecapture', 'onmousedown', 'onmouseenter', 'onmouseleave', 'onmousemove', 'onmouseout', 'onmouseover', 'onmouseup', 'onmousewheel', 'onmove', 'onmoveend', 'onmovestart', 'onpaste', 'onpropertychange', 'onreadystatechange', 'onreset', 'onresize', 'onresizeend', 'onresizestart', 'onrowenter', 'onrowexit', 'onrowsdelete', 'onrowsinserted', 'onscroll', 'onselect', 'onselectionchange', 'onselectstart', 'onstart', 'onstop', 'onsubmit', 'onunload');
       $ra = array_merge($ra1, $ra2);

       $found = true; // keep replacing as long as the previous round replaced something
       while ($found == true) {
          $val_before = $val;
          for ($i = 0; $i < sizeof($ra); $i++) {
             $pattern = '/';
             for ($j = 0; $j < strlen($ra[$i]); $j++) {
                if ($j > 0) {
                   $pattern .= '(';
                   $pattern .= '(&#[xX]0{0,8}([9ab]);)';
                   $pattern .= '|';
                   $pattern .= '|(&#0{0,8}([9|10|13]);)';
                   $pattern .= ')*';
                }
                $pattern .= $ra[$i][$j];
             }
             $pattern .= '/i';
             $replacement = substr($ra[$i], 0, 2).'<x>'.substr($ra[$i], 2); // add in <> to nerf the tag
             $val = preg_replace($pattern, $replacement, $val); // filter out the hex tags

             if ($val_before == $val) {
                // no replacements were made, so exit the loop
                $found = false;
             }
          }
       }

       return $val;
    }
	
	# 用指定元素指定字段作为（替换或新增）key
    public static function arrayChangeKey($array, $key)
    {
        $result = [];
        if (empty($array)) {
            return $result;
        }
        foreach ($array as &$item) 
		{
            $result[$item[$key]] = $item;
        }
        return $result;
    }
	
    /**
     * 抽取多维数组的某个元素,组成一个新数组,使这个数组变成一个扁平数组
     * 使用方法:
     * <code>
     * <?php
     * $fruit = [['apple' => 2, 'banana' => 3], ['apple' => 10, 'banana' => 12]];
     * $banana = array_values($fruit, 'banana');
     * print_r($banana);
     * //outputs: [0 => 3, 1 => 12];
     * ?>
     * </code>
     *
     * @access public
     * @param array $value 被处理的数组
     * @param string $key 需要抽取的键值
     * @return array
     */
    public static function arrayColumn($array, $key)
    {
		return array_column($array, $key);
		
		/*
        $result = [];
        foreach ($array as $item) 
		{
            if (is_array($item) && isset($item[$key])) {
                $result[] = $item[$key];
            } else {
                break;
            }
        }
        return $result;*/
    }

    /**
     * Determine if a given string contains a given substring.
     *
     * @param  string $haystack
     * @param  string|array $needles
     * @return bool
     */
    public static function contains($haystack, $needles)
    {
        foreach ((array)$needles as $needle) {
            if ($needle != '' && mb_strpos($haystack, $needle) !== false) {
                return true;
            }
        }

        return false;
    }

}